﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using ScrumTable.Common;
using ScrumTable.Common.Logging;
using ScrumTable.Common.Threading;
using ScrumTable.DL.Data.Common;
using ScrumTable.DL.Data.Generic.Cache;
using ScrumTable.DL.Data.Generic.Properties;

#endregion

namespace ScrumTable.DL.Data.Generic.Services
{
    /// <summary>
    /// This class is used to load the settings of a work item.
    /// </summary>
    internal class DataSaveManager : DisposeBase, IDataSaveManager
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private readonly MessageQueue<CacheDataChanges> _saveSyncQueue;
        private readonly GenericDataContextBase _context;
        private readonly DataSaveTimer _timer;

        private readonly IDataSaver _asyncDataSaver;
        private readonly IDataSyncDispatcher _asyncDispatcher;
        private readonly AsyncDataContextInfo _asyncDataContextInfo;

        private volatile bool _callPending;
        private volatile bool _doAbort = false;

        /**
         * The follwoing variables MUST BE USED in the main UI thread only.
         */
        private bool _finishCallDelayed = false;
        private bool _finishCallPending = false;
        private AsyncCallback _finishCallback;

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new DataSaveProcess instance, no public constructor
        /// needed.
        /// </summary>
        internal DataSaveManager(GenericDataContextBase context)
        {
            PreCondition.AssertNotNull(context, "context");

            _context = context;
            
            _asyncDataSaver = _context.DataSaver;
            _asyncDispatcher = _context.DataSyncDispatcher;
            _asyncDataContextInfo = new AsyncDataContextInfo
                {
                    ConnectionManager = context.Connection.ConnectionManager,
                    ConnectionMoniker = context.Connection.ConnectionMoniker,
                    Schema = context.Schema
                };

            _saveSyncQueue = new MessageQueue<CacheDataChanges>();
            _timer = new DataSaveTimer(OnTick);
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Starts the asynchronous data save/sync process.
        /// </summary>
        /// <remarks>This procedure must be called from UI thread.</remarks>
        public void Start()
        {
            StartTimer();
        }

        /// <summary>
        /// Stops the asynchronous data save/sync process.
        /// </summary>
        /// <param name="callbackEvent">Specifies the callback which is fired after the shutdown process has been completed.</param>
        /// <remarks>This procedure must be called from main UI thread.</remarks>
        public void Stop(AsyncCallback callbackEvent)
        {
            PreCondition.AssertNotNull(callbackEvent, "callbackEvent");

            _finishCallback = callbackEvent;

            if (_callPending) 
            {
                // if there is a save call pending, you have to wait 'till it's completed
                _finishCallDelayed = true;
            }
            else
            {
                StartFinishCall();
            }
        }

        /// <summary>
        /// Releases managed (c# or .net) resources.
        /// </summary>
        protected override void ReleaseManagedResources()
        {
            _doAbort = true;
            _timer.Dispose();
            _saveSyncQueue.Abort();
        }

        private void StartTimer()
        {
            _callPending = false;

            if (!((IDisposableObject)this).IsDisposing)
            {
                Logger<DataSaveManager>.Debug(Resources.LogDataSvMngrStart);    
                _timer.Start();
            }
        }

        private void ReStartTimer(bool accepted)
        {
            if (_finishCallPending)
            {
                // it's the last time we called the ReStart timer procedure, 
                // the StartTimer() procedure MUST NOT be called anymore because
                // we do not start the timer anymore, if it's the finish call
                ExecuteFinishCallback(accepted);
            }
            else
            {
                if (!_finishCallDelayed)
                {
                    // corresponds to the default behaviour, we have to start the
                    // process again and again...
                    StartTimer();
                }
                else
                {
                    // corresponds to the finish behaviour, we have to call the
                    // process the last time
                    StartFinishCall();
                }
            }
        }

        private void StartFinishCall()
        {
            _finishCallPending = true;
            OnCheckChangesFromCache();
        }

        private void ExecuteFinishCallback(bool accepted)
        {
            _finishCallback(new DefaultCancellableDataResult(Restart, accepted));
        }

        private void Restart()
        {
            _finishCallDelayed = false;
            _finishCallPending = false;
            _finishCallback = null;
            _doAbort = false;
            StartTimer();
        }

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        /// <remarks>
        /// The tick event is executed in a thread of the windows thread pool.
        /// </remarks>
        private void OnTick()
        {
            Logger<DataSaveManager>.Debug(Resources.LogDataSvMngrOnTick);
            _asyncDispatcher.BeginInvoke(OnCheckChangesFromCache);
        }

        /// <remarks>
        /// The tick data sync call is executed in the main message loop, so
        /// the GUI will block if there is a too long process performed. This
        /// call is synchronized with the GUI, so no additional synchronization
        /// with the data cache is required.
        /// </remarks>
        private void OnCheckChangesFromCache()
        {
            if (_doAbort || ((IDisposableObject)_context).IsDisposed) { return; }
            _callPending = true;
            if (_finishCallPending) { _doAbort = true; }

            Logger<DataSaveManager>.DebugFormat(Resources.LogDataSvMngrOnCheckChangesFromCache, _context.Connection.IsConnected);

            using (new TimeLogger<DataSaveManager>(true, Resources.DataSvMngrGetChanges))
            {
                try
                {
                    if (_context.Connection.IsConnected)
                    {
                        CacheDataChanges changes = _context.ExportChanges();

                        if (changes != null)
                        {
                            if (_asyncDataSaver.UseAsync)
                            {
                                _saveSyncQueue.Enqueue(new MessageQueueCall<CacheDataChanges>(OnSaveChangesToServerAsync), changes);
                            }
                            else
                            {
                                OnSaveChangesToServer(changes);
                            }
                        }
                    }
                    else
                    {
                        ReStartTimer(false);
                    }
                }
                catch (Exception e)
                {
                    Logger<DataSaveManager>.Error(Resources.ExcDataSaveMngrGetChanges, e);
                    ReStartTimer(false);
                }
            }
        }

        /// <remarks>
        /// The changes will be saved to the server in the separate
        /// save/sync message queue.
        /// </remarks>
        private void OnSaveChangesToServerAsync(CacheDataChanges changes)
        {
            Logger<DataSaveManager>.DebugFormat(Resources.LogDataSvMngrOnSaveChangesToServerAsync, changes.ChangedElementCount);

            try
            {
                _asyncDataSaver.SyncChangesAsync(_asyncDataContextInfo, changes);
            }
            catch (Exception e)
            {
                // catch exceptions from SyncChangesAsync() procedure. this is important
                // because of the sync process. Otherwise the process aborts.
                changes.MarkChangesAsError(e);
                Logger<DataSaveManager>.Error(Resources.ExcDataSaveMngrAcceptChangesAsync, e);
            }

            _asyncDispatcher.BeginInvoke(() => SyncChangesFromServer(changes));
        }

        /// <remarks>
        /// The changes will be saved to the server in the separate
        /// save/sync message queue.
        /// </remarks>
        private void OnSaveChangesToServer(CacheDataChanges changes)
        {
            Logger<DataSaveManager>.DebugFormat(Resources.LogDataSvMngrOnSaveChangesToServer, changes.ChangedElementCount);

            try
            {
                _asyncDataSaver.SyncChanges(_context, changes);
            }
            catch (Exception e)
            {
                // catch exceptions from SyncChangesAsync() procedure. this is important
                // because of the sync process. Otherwise the process aborts.
                changes.MarkChangesAsError(e);
                Logger<DataSaveManager>.Error(Resources.ExcDataSaveMngrAcceptChanges, e);
            }

            SyncChangesFromServer(changes);
        }

        private void SyncChangesFromServer(CacheDataChanges changes)
        {
            /**
             * This call is synchronized with the GUI,
             * so no additional synchronization with the
             * data cache is required.
             */
            Logger<DataSaveManager>.Debug(Resources.LogDataSvMngrOnSaveChangesToServerCompleted);

            using (new TimeLogger<DataSaveManager>(true, Resources.DataSvMngrAcceptChanges))
            {
                try
                {
                    if (!((IDisposableObject) _context).IsDisposed)
                    {
                        /**
                         * Call the accept changes procedure. This procedure must set the
                         * AcceptChanges flags and store the changes on the back-end system.
                         */
                        _context.ImportChanges(changes);
                        ReStartTimer(changes.Accepted);
                    }
                    else
                    {
                        /**
                         * The data context died in meantime. Thus restarting the timer
                         * is not needed. Just log the disposed situation.
                         */
                        Logger<DataSaveManager>.Error(Resources.ExcDataSaveMngrObjDisposed);
                    }
                }
                catch (Exception e)
                {
                    Logger<DataSaveManager>.Error(Resources.ExcDataSaveMngrAcceptChanges, e);
                    ReStartTimer(false);
                }
            }
        }

        #endregion
    }
}